package validation

import (
	"context"
	"errors"
	"fmt"
	"github.com/jmespath/go-jmespath"
	"github.com/open-policy-agent/opa/rego"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/util/match"
	"reflect"
	"regexp"
	"sigs.k8s.io/yaml"
)

func Iterator(ctx context.Context, elem interface{}, rulePath, ruleValue string, isReverse bool) bool {
	val := reflect.ValueOf(elem)

	tp := reflect.TypeOf(elem)
	if !val.IsValid() || val.IsZero() {
		return false
	}
	switch tp.Kind() {
	case reflect.Slice:
		length := val.Len()
		for i := 0; i < length; i++ {
			if isReverse {
				if Iterator(ctx, val.Index(i).Interface(), rulePath, ruleValue, isReverse) {
					return true
				}
			} else {
				if !Iterator(ctx, val.Index(i).Interface(), rulePath, ruleValue, isReverse) {
					return false
				}
			}
		}
	case reflect.Map:
		keys := val.MapKeys()
		for _, v := range keys {
			value := val.MapIndex(v)
			if isReverse {
				if Iterator(ctx, value.Interface(), rulePath, ruleValue, isReverse) {
					return true
				}
			} else {
				if !Iterator(ctx, value.Interface(), rulePath, ruleValue, isReverse) {
					return false
				}
			}
		}
	case reflect.Struct:
		for i := 0; i < val.NumField(); i++ {
			f := val.Field(i)
			logs.Error(f.Interface())
		}
	case reflect.String:
		scope := ruleValue

		str, ok := elem.(string)

		if !ok {
			return false
		}
		o, err := regexp.Match(ruleValue, []byte(str))
		if err != nil {
			logs.Errorf("Scope validation error %v", err)
		}
		if isReverse {
			if o {
				logs.Debugf("value: %s match scope: %s, ok\n", str, scope)
				return true
			}
		} else {
			if !o {
				logs.Debugf("value: %s not match with scope: %s, path: %s failed\n", str, scope, rulePath)
				return false
			}
			logs.Debugf("value: %s match scope: %s, ok\n", str, scope)
		}

	default:
		return isReverse
	}

	return !isReverse
}

// checkRule return result, detailedMessages
func checkRule(ctx context.Context, name string, parameters, inputObject map[string]interface{}) error {
	rules := storage.Storage.GetRegoRules()
	rule, ok := rules[name]
	if !ok {
		return fmt.Errorf("rego rule %s not found", name)
	}
	input := map[string]interface{}{
		"review":     inputObject,
		"parameters": parameters,
	}
	results, err := rule.Eval(ctx, rego.EvalInput(input))
	if err != nil {
		// Handle evaluation error.
		return fmt.Errorf("check rego rule %s failed %s", name, err)
	}
	if len(results) == 0 {
		// Handle undefined result.
		return fmt.Errorf("check rego rule %s failed, result zero. (exec failed?)", name)
	}
	result, ok := results[0].Bindings["x"].([]interface{})
	if !ok {
		return fmt.Errorf("check rego rule %s failed, result exist, but unexpected. (exec failed?)", name)
	}
	if len(result) > 0 {
		var details []string

		for _, r := range result {
			details = append(details, r.(map[string]interface{})["msg"].(string))
		}

		return fmt.Errorf("validation failed: rego rule %s details %v", name, details)
	}
	return nil

}

func ValidateResource(ctx context.Context, original map[string]interface{}, requestData common.ARFields) error {
	scopes := storage.Storage.GetScopeItems(Validation)
	for _, scope := range scopes {
		err := validate(ctx, requestData, scope, original)
		if err != nil {
			return err
		}
	}
	return nil
}

func validate(ctx context.Context, requestData common.ARFields, item v1alpha1.Item, original map[string]interface{}) error {
	allowed := match.CheckMatching(ctx, requestData, item.Match)
	if !allowed {
		return nil
	}

	err := CheckRegoRule(ctx, original, item)
	if err != nil {
		return err
	}

	if item.Rule.Simples == nil {
		return nil
	}

	err = CheckSimplesRule(ctx, item, requestData)
	if err != nil {
		return err
	}
	return nil
}

func CheckSimplesRule(ctx context.Context, item v1alpha1.Item, requestData common.ARFields) error {
	var simplesErr error

	for _, simple := range item.Rule.Simples {

		var object map[string]interface{}
		if requestData.Operation != "DELETE" {
			object = requestData.Object
		} else {
			object = requestData.OldObject
		}

		result, err := jmespath.Search(simple.Path, object)
		if err != nil {
			continue
		}

		validationSuccess := validateValue(ctx, result, simple.Path, simple.Value, simple.Action)
		if !validationSuccess {

			message := simple.Message
			if simple.Message == "" {
				message = fmt.Sprintf("validation failed: rule %s path %s", simple.Value, simple.Path)
			}

			if simplesErr == nil {
				simplesErr = errors.New(message)
				continue
			}

			simplesErr = fmt.Errorf("%s; %s", simplesErr.Error(), message)
		}
	}
	if simplesErr != nil {
		return simplesErr
	}
	return nil
}

func CheckRegoRule(ctx context.Context, original map[string]interface{}, item v1alpha1.Item) error {
	if item.Rule.Rego.Template == "" {
		return nil
	}

	var params map[string]interface{}
	if err := yaml.Unmarshal([]byte(item.Rule.Rego.Parameters), &params); err != nil {
		return fmt.Errorf("failed to unmarshal yaml rego.parameters with error: %v. source: %q", err, item.Rule.Rego.Parameters)
	}

	err := checkRule(ctx, item.Rule.Rego.Template, params, original)
	if err != nil {
		return err
	}

	return nil
}

func validateValue(ctx context.Context, result interface{}, path, value, action string) bool {
	resultSlice, isSlice := result.([]interface{})
	if result != nil && value == ".*" && !isSlice {
		if action == Deny {
			return false
		}
		if action == Allow {
			return true
		}
	}
	if isSlice && len(resultSlice) != 0 && value == ".*" {
		if action == Deny {
			return false
		}
		if action == Allow {
			return true
		}
	}

	if action == Deny {
		if Iterator(ctx, result, path, value, true) {
			logs.Debugf("validation failed: rule %s path %s", value, path)
			return false
		}
	}
	if action == Allow {
		if !Iterator(ctx, result, path, value, false) {
			logs.Debugf("validation failed: rule %s path %s", value, path)
			return false
		}
	}

	return true
}
